<?php

Raise::load('core.RaiseHelper');
Raise::load('core.RaiseCookie');
Raise::load('core.IRaiseSessionStore');
Raise::load('core.RaiseCollection');
Raise::load('core.RaiseApplication');
Raise::load('core.RaiseHashDigest');
Raise::load('core.RaiseDateTime');
Raise::load('core.RaiseConvert');
Raise::load('core.RaiseSessionFileStore');

/**
 * RaiseSession helper class
 * defines the current session
 *
 * @author Sam-Mauris Yong <hellclanner at live dot com>
 * @license http://www.opensource.org/licenses/bsd-license New BSD License
 * @package Raise.Core.Session
 * @since 1.2
 */
class RaiseSession extends RaiseHelper {

    /**
     * The default Session Name to use when session name is not specified.
     * This is different from the default PHP session name 'PHPSESSID'
     */
    const DEFAULT_SESSION_NAME = 'raiseSID';

    /**
     * The key name where the session signatured is stored within the session
     */
    const SIGNATURE_KEY = '?!:S1GNATUr3K3Y';

    /**
     * The key name where the session internal expiry is stored within the session
     */
    const INTERNAL_EXPIRY_KEY = '?!:1NTERN4l3XP1rYKEY';

    /**
     * Defines the Session Cookie
     * @var RaiseCookie
     */
    private $cookie;

    /**
     * Flag if the session has started
     * @var boolean
     */
    private $started = false;

    /**
     * The session data
     * @var RaiseCollection
     */
    private $data;

    /**
     * The RaiseApplication that started this RaisePage
     * @var RaiseApplication
     */
    private $application;

    /**
     * The IRaiseSessionStore that the session data will be saved to.
     * @var IRaiseSessionStore
     */
    private $store;

    /**
     * Create a new RaiseSession object
     * @param IRaiseSessionStore $store
     */
    public function __construct($store = null) {
        $this->name = self::DEFAULT_SESSION_NAME;
        $this->cookie = new RaiseCookie(self::DEFAULT_SESSION_NAME, null);
        $this->store = $store;
    }

    /**
     * Load the store that can be used with RaiseSession
     * @param RaiseSettings $settings The settings to load the session store from
     * @return IRaiseSessionStore
     */
    public static function loadStore($settings) {
        $class = $settings->get('Session', 'store');
        $data = $settings->get('Session', 'storeData');
        $store = false;
        if ($class && class_exists($class) && $data) {
            $store = new $class($data);
        }
        if ($store instanceof IRaiseSessionStore) {
            return $store;
        }
        return null;
    }

    /**
     * Get or set the RaiseApplication that started this page
     * @param RaiseApplication $app (optional) If set, the new value will be set.
     * @return RaiseApplication
     */
    public function application($app = null) {
        if (func_num_args() == 1) {
            if (!($app instanceof RaiseApplication)) {
                throw new RaiseInvalidArgumentException('RaiseSession::application() expecting $app to be an instance of RaiseApplication, ' . RaiseVariable::typeInfo($app) . ' given instead.');
            }
            $this->application = $app;
        }
        return $this->application;
    }

    /**
     * Tell whether the session has started for this client or not.
     * @return boolean
     */
    public function started() {
        return $this->started;
    }

    /**
     * Get the cookie that is representing the session
     * @return RaiseCookie
     */
    public function cookie() {
        return $this->cookie;
    }

    /**
     * Start the session
     */
    public function start() {
        if ($this->started) {
            return;
        }

        if ($this->store instanceof IRaiseSessionStore) {
            session_set_save_handler(array($this->store, 'open'), array($this->store, 'close'), array($this->store, 'read'), array($this->store, 'write'), array($this->store, 'destroy'), array($this->store, 'garbageCollect'));
        }

        if ($this->cookie) {
            if (strtolower($this->cookie->domain()) == 'localhost') {
                $this->cookie->domain(''); // fix for localhost cookies
            }
            session_set_cookie_params($this->cookie->expire()->toTimestamp(), $this->cookie->path(), $this->cookie->domain(), $this->cookie->secure(), $this->cookie->httpOnly());
        }

        if ($this->cookie && $this->cookie->name()) {
            session_name($this->cookie->name());
        } else {
            // default session
            session_name(RaiseSession::DEFAULT_SESSION_NAME);
        }

        if (!$_COOKIE || !array_key_exists(session_name(), $_COOKIE)) {
            if ($this->application()) {
                $this->application()->plugin()->callHook('raise.session.start', $this);
            }
            session_id($this->generateId());
        } else {
            // resuming session
            if ($this->application()) {
                $this->application()->plugin()->callHook('raise.session.resume', $this);
            }
        }

        $ok = @session_start();
        // Session start failed, probably this bug:
        // http://thephpcode.blogspot.com/2009/09/session-denial-session-id-contains.html
        if (!$ok) {
            $this->antiFixation();
            session_start();
        }

        $this->data = new RaiseCollection($_SESSION);

        if ($this->application()) {
            $this->application()->plugin()->callHook('raise.session.load', $this);
        }
        $this->started = true;

        /**
         * This part of the code is to prevent Session Hijacking by checking the user agent
         * of the client. If the user agent is not the same as we have it, it might mean that
         * another user using another client is trying to use the same session
         * See link for more details
         * @link http://phpsec.org/projects/guide/4.html
         */
        if ($this->application()) {
            $testSignature = RaiseHashDigest::sha256($this->application->client()->userAgent());
            if ($this->data->keyExists(self::SIGNATURE_KEY)) {
                // check whether signature is the same or not
                if ($this->data[self::SIGNATURE_KEY] != $testSignature) {
                    // the signature is not the same! might be session hijacking
                    @session_write_close();
                    session_id($this->generateId());
            var_dump('2start');
                    session_start();
                    $this->data[self::SIGNATURE_KEY] = $testSignature;
                    return;
                }
            } else {
                $this->data[self::SIGNATURE_KEY] = $testSignature;
            }

            if ($this->application()->request()->method() == 'GET') {
                /**
                 * The session will expire internally
                 */
                if ($this->data->keyExists(self::INTERNAL_EXPIRY_KEY) && $this->data[self::INTERNAL_EXPIRY_KEY] instanceof RaiseDateTime) {
                    $expiry = $this->application->settings()->get('Session', 'antiFixationInterval') ? $this->application->settings()->get('Session', 'antiFixationInterval') : 120;
                    if (RaiseDateTime::now()->subtract($this->data[self::INTERNAL_EXPIRY_KEY])->totalSeconds() > $expiry) {
                        $this->antiFixation();
                        $this->data[self::INTERNAL_EXPIRY_KEY] = RaiseDateTime::now();
                    }
                } else {
                    $this->data[self::INTERNAL_EXPIRY_KEY] = RaiseDateTime::now();
                }
            }


        }
    }

    /**
     * Generate an ID for the session
     * @return string
     */
    private function generateId() {
        $id = null;
        do {
            $id = RaiseConvert::arbitraryBase($this->hashCode() . RaiseHashDigest::sha256(mt_rand() . RaiseDateTime::getMicrotime() . $this->hashCode()), 16, 36);
        } while ($this->store != null && $this->store->sessionExist($id));
        return $id;
    }

    /**
     * Prevent Session Fixation (Pre-hijacking) by regenerating
     * a new Session ID. Call this method after user has logged in
     * @link http://www.acros.si/papers/session_fixation.pdf
     */
    public function antiFixation() {
        if ($this->started) {
            session_destroy();
            $this->id($this->generateId());
            var_dump('3start');
            session_start();
        } else {
            $this->id($this->generateId());
        }
    }

    /**
     * Get or set the Session ID
     * @param string $newId (optional) New session ID to set
     * @return string
     */
    public function id($newId = null) {
        if (func_num_args() == 1) {
            session_id($newId);
        }
        return session_id();
    }

    /**
     * Get the Session Name
     * @param string $newId (optional) New session ID to set
     * @return string
     */
    public function name($name = null) {
        if (func_num_args() == 1) {
            if ($this->started) {
                throw new RaiseInvalidRequestException('Session name cannot be changed when session is in progress i.e. started.');
            }
            session_name($name);
        }
        return session_name();
    }

    /**
     * Close the session
     */
    public function close() {
        if (!$this->started) {
            throw new RaiseInvalidRequestException('Session cannot be closed because it has not started.');
            return;
        }

        session_write_close();
        $this->started = false;

        if ($this->application()) {
            $this->application()->plugin()->callHook('raise.session.close', $this);
        }
    }

    /**
     * Clear all session variables, but maintain the session
     * @static
     */
    public function clear() {
        if (!$this->started) {
            throw new RaiseInvalidRequestException('Session cannot be cleared because it has not started.');
            return;
        }
        $_SESSION = array();
        if ($this->application()) {
            $this->application()->plugin()->callHook('raise.session.clear', $this);
        }
    }

    /**
     * Set a session variable
     * @param string $f The name of the variable
     * @param mixed $v The variable
     * @static
     */
    public function set($f, $v) {
        if (!$this->started) {
            throw new RaiseInvalidRequestException('Session variables cannot be assigned since the session has not started.');
            return;
        }
        $_SESSION[$f] = $v;
    }

    /**
     * Get a session variable
     * @param string $f The name of the variable to fetch
     * @return mixed
     */
    public function get($f) {
        if (!$this->started) {
            throw new RaiseInvalidRequestException('Session variables cannot be assigned since the session has not started.');
            return;
        }
        if(array_key_exists($f, $_SESSION)){
            return $_SESSION[$f];
        }
        return null;
    }

    /**
     * Remove a session variable
     * @param string $f The name of the variable to remove
     */
    public function remove($f) {
        if(array_key_exists($f, $_SESSION)){
            unset($_SESSION[$f]);
        }
    }

    public function dispose() {
        if ($this->started) {
            $this->close();
        }
    }

}